Hermes api_server 平台适配方案
来源:分析
gateway/platforms/api_server.py源码(2903行)
整理时间:2026-05-04
一、平台定位
api_server 是 Hermes 唯一一个完全自定义消息格式的平台适配器。它将 Hermes 的 Agent 能力以 OpenAI Chat Completions API 格式对外暴露,任何兼容 OpenAI API 的前端(Open WebUI、LobeChat、LibreChat、AnythingLLM、NextChat、ChatBox 等)都可以直接接入。
Open WebUI / LobeChat / ChatBox
│
│ POST /v1/chat/completions
▼
┌─────────────────────┐
│ Hermes api_server │ ← gateway/platforms/api_server.py
│ (HTTP + SSE) │
└────────┬────────────┘
│ 内部调用 AIAgent
▼
┌─────────┐
│ Hermes │
│ Agent │
└─────────┘
二、接口清单(12个端点)
| 方法 | 路径 | 说明 |
|---|---|---|
GET |
/health |
基础健康检查 |
GET |
/health/detailed |
详细状态(含平台/进程/PID) |
GET |
/v1/models |
可用模型列表 |
GET |
/v1/capabilities |
机器可读的能力描述 |
POST |
/v1/chat/completions |
核心:OpenAI 兼容 Chat Completions |
POST |
/v1/responses |
OpenAI Responses API(有状态) |
GET |
/v1/responses/{response_id} |
查询历史响应 |
DELETE |
/v1/responses/{response_id} |
删除历史响应 |
POST |
/v1/runs |
启动异步 Run(立即返回 run_id,202) |
GET |
/v1/runs/{run_id} |
查询 Run 状态 |
GET |
/v1/runs/{run_id}/events |
SSE 事件流 |
POST |
/v1/runs/{run_id}/stop |
中断正在运行的 Agent |
三、认证机制
3.1 Token 校验
def _check_auth(self, request: web.Request) -> Optional[web.Response]:
if not self._api_key:
return None # 未配置 API Key → 允许所有(仅本地使用)
auth_header = request.headers.get("Authorization", "")
# 标准 Bearer Token 格式
- Header 格式:
Authorization: Bearer <token> - 无 Key 时:允许所有请求(本地调试模式)
- 有 Key 时:校验失败返回
401 Unauthorized
3.2 CORS 配置
_CORS_HEADERS = {
"Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "Authorization, Content-Type, Idempotency-Key",
}
支持浏览器端直接请求,可配置允许的 Origin 列表。
四、消息格式支持
4.1 Chat Completions 请求格式
POST /v1/chat/completions
{
"model": "hermes-agent",
"messages": [
{"role": "system", "content": "你是一个有用的助手"},
{"role": "user", "content": "你好"}
],
"stream": false
}
Content 字段支持两种形式:
- 字符串(最常见):
{"role": "user", "content": "你好"}
- 多模态数组(含图片):
{
"role": "user",
"content": [
{"type": "text", "text": "这张图里有什么?"},
{"type": "image_url", "image_url": {"url": "https://example.com/image.jpg"}}
]
}
支持的 part 类型:
| Type | 支持情况 | 说明 |
|---|---|---|
text / input_text / output_text |
✅ | 文本内容 |
image_url / input_image |
✅ | 图片(http/https/data:image URL) |
file / input_file |
❌ 主动拒绝 | 抛出 unsupported_content_type 异常 |
| 未知类型 | ❌ 主动拒绝 | 抛出 unsupported_content_type 异常 |
4.2 Chat Completions 响应格式
非流式:
{
"id": "chatcmpl-xxx",
"object": "chat.completion",
"created": 1714992000,
"model": "hermes-agent",
"choices": [{
"index": 0,
"message": {"role": "assistant", "content": "你好,有什么可以帮你?"},
"finish_reason": "stop"
}],
"usage": {
"prompt_tokens": 10,
"completion_tokens": 20,
"total_tokens": 30
}
}
流式(SSE):
data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":null}]}
data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"你"},"finish_reason":null}]}
data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"好"},"finish_reason":null}]}
data: [DONE]
自定义事件(工具进度):
event: hermes.tool.progress
data: {"toolCallId":"xxx","status":"completed","output":"..."}
五、Session 管理
5.1 无状态模式(默认)
每次请求独立,Agent 无记忆。
5.2 有状态模式(通过 Header)
X-Hermes-Session-Id: agent:main:api:tenant:123:user:456
Session ID 格式遵循 Hermes 标准:
| 层级 | 格式 | 说明 |
|---|---|---|
| 全局记忆 | agent:main:api:global |
所有用户共享 |
| 租户共享 | agent:main:api:tenant:{id}:shared |
同一租户用户共享 |
| 成员私有 | agent:main:api:tenant:{id}:user:{id} |
单个用户私有记忆 |
5.3 Responses API(持久化会话)
通过 previous_response_id 实现多轮对话状态:
POST /v1/responses
{
"model": "hermes-agent",
"previous_response_id": "resp_abc123",
"modalities": ["text"],
"input": {"messages": [{"role": "user", "content": "继续"}]}
}
历史响应通过 SQLite LRU 存储(默认100条),路径 ~/.hermes/response_store.db。
六、Runs 接口(异步模式)
适合需要立即拿到 run_id、后续轮询状态或订阅 SSE 事件的场景:
Step 1: POST /v1/runs → 立即返回 202 + run_id
Step 2: GET /v1/runs/{run_id} → 轮询状态
Step 3: GET /v1/runs/{run_id}/events → SSE 订阅事件
Step 4: POST /v1/runs/{run_id}/stop → 中断任务
Runs SSE 事件类型:
| 事件名 | 触发时机 |
|---|---|
agent.started |
Agent 开始处理 |
agent.output |
最终输出完成 |
agent.stopped |
被 stop 打断 |
agent.error |
运行异常 |
tool.progress |
工具执行进度 |
tool.started |
工具开始 |
tool.completed |
工具完成 |
tool.error |
工具异常 |
七、与微信 Typing 的集成方案
7.1 现状
微信平台支持 send_typing / stop_typing,通过 ilink/bot/sendtyping 接口推送"正在输入"状态。Hermes 在 LLM 开始处理时自动调用 send_typing,处理完成后调用 stop_typing。
7.2 api_server 扩展方案
api_server 是纯 HTTP 请求/响应,无法主动推送。扩展方案:
┌──────────────────────────────────────────────────────────────┐
│ 扩展架构 │
│ │
│ App ────────── WebSocket 连接 ────► api_server /ws/subscribe │
│ │
│ Hermes ────(send_typing)────► api_server ────广播──► App │
│ │ │ │
│ └── typing 事件推送 ◄───────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
新增端点:
WebSocket /ws/subscribe?token=<user_token>&tenant_id=<id>
推送消息格式:
{"type": "typing", "chat_id": "xxx", "status": "start|stop"}
消息订阅管理:
class TypingBroadcast:
def __init__(self):
# chat_id -> list of WebSocket connections
self._subscribers: Dict[str, List[WebSocket]] = {}
async def broadcast(self, chat_id: str, status: str):
for ws in self._subscribers.get(chat_id, []):
await ws.send_json({"type": "typing", "chat_id": chat_id, "status": status})
八、Token 身份认证方案(完整版)
8.1 Token 生成
import secrets
def generate_token() -> str:
"""生成64位安全的随机Token"""
return secrets.token_bytes(32).hex() # 64字符
8.2 数据库设计
-- 租户表
CREATE TABLE tenants (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
app_key VARCHAR(64) NOT NULL UNIQUE COMMENT 'App唯一标识',
is_active TINYINT(1) DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_app_key (app_key)
);
-- 用户表
CREATE TABLE users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
tenant_id BIGINT NOT NULL,
username VARCHAR(64) NOT NULL,
password_hash VARCHAR(255),
nickname VARCHAR(100),
avatar_url VARCHAR(512),
is_active TINYINT(1) DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_tenant_username (tenant_id, username),
FOREIGN KEY (tenant_id) REFERENCES tenants(id)
);
-- Token表
CREATE TABLE auth_tokens (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
token VARCHAR(64) NOT NULL UNIQUE COMMENT '64位随机字符串',
user_id BIGINT NOT NULL,
tenant_id BIGINT NOT NULL,
device_info VARCHAR(255),
client_version VARCHAR(32),
last_active_at DATETIME DEFAULT CURRENT_TIMESTAMP,
expires_at DATETIME COMMENT 'NULL=永不过期',
is_revoked TINYINT(1) DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (tenant_id) REFERENCES tenants(id),
INDEX idx_token (token),
INDEX idx_user (user_id)
);
-- API调用日志
CREATE TABLE api_call_logs (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
call_id VARCHAR(64) NOT NULL UNIQUE,
tenant_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
provider VARCHAR(32) NOT NULL,
model VARCHAR(64) NOT NULL,
is_custom_key TINYINT(1) DEFAULT 0,
custom_key_id BIGINT,
input_tokens INT DEFAULT 0,
output_tokens INT DEFAULT 0,
latency_ms INT,
status_code VARCHAR(32),
error_msg TEXT,
ip_address VARCHAR(45),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_tenant_time (tenant_id, created_at)
);
8.3 认证中间件伪代码
async def auth_middleware(request, call_next):
token = request.headers.get("Authorization", "").replace("Bearer ", "")
if not token:
return Response(401, body="Missing token")
row = await db.query("""
SELECT u.id, u.tenant_id, u.is_active, t.is_active
FROM auth_tokens r
JOIN users u ON r.user_id = u.id
JOIN tenants t ON r.tenant_id = t.id
WHERE r.token = ? AND r.is_revoked = 0
AND u.is_active = 1 AND t.is_active = 1
AND (r.expires_at IS NULL OR r.expires_at > NOW())
""", token)
if not row:
return Response(401, body="Invalid token")
# 注入到请求上下文,应用层无法伪造
request.user_id = row["id"]
request.tenant_id = row["tenant_id"]
return await call_next(request)
九、媒体类型限制与处理策略
| 媒体类型 | api_server 支持 | 扩展方案 |
|---|---|---|
| 文字 | ✅ | — |
| 图片 URL | ✅ | 直接透传 |
| 语音/音频 | ❌ | 先上传 OSS,URL 注入 content |
| 视频 | ❌ | 先上传 OSS,URL 注入 content |
| 文件 | ❌(主动抛异常) | 先上传 OSS,URL 注入 content |
统一扩展思路:所有非图片媒体 → 上传至 OSS/CDN → 得到 URL → 作为 text content 的一部分发送给 Agent。响应中的多模态内容(语音/图片)通过 TTS/图片生成处理后再推送给 App。
十、配置参考
platforms:
api_server:
enabled: true
host: "0.0.0.0"
port: 8642
api_key: "your-secret-key" # 可选,不填则允许所有(本地)
cors_origins: # 可选,CORS 白名单
- "https://your-app.com"
- "https://admin.example.com"
model_name: "hermes-agent" # 广播的模型名称
max_request_bytes: 1_000_000 # 请求体大小限制(默认1MB)
十一、关键源码位置
| 功能 | 位置 |
|---|---|
| 入口 / 路由注册 | api_server.py 末尾 add_routes() |
| 认证 | _check_auth() 第 668 行 |
| 消息内容标准化 | _normalize_multimodal_content() 第 132 行 |
| Chat Completions 处理 | _handle_chat_completions() |
| SSE 流式响应 | _handle_chat_completions() 内 stream=True 分支 |
| Runs 异步管理 | _run_streams / _active_run_agents / _active_run_tasks |
| ResponseStore(LRU) | ResponseStore 类 第 282 行 |
| Agent 创建 | _create_agent() 第 712 行 |
十二、App 会话交互日志管理
12.1 设计目标
对 App 用户与 AI 的每次会话交互进行完整记录,用于:
- 审计追溯:记录谁在什么时间问了什么、得到了什么回答
- 会话恢复:支持用户查看历史对话上下文
- 数据分析:统计用户行为、热门问题、响应质量
- 异常排查:定位用户反馈问题的完整上下文
12.2 日志表设计
-- 会话表(一个对话会话 = 一个 topic)
CREATE TABLE chat_sessions (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
session_id VARCHAR(64) NOT NULL UNIQUE COMMENT '会话唯一ID',
tenant_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
title VARCHAR(256) COMMENT '会话标题(取首条消息前30字)',
status VARCHAR(16) DEFAULT 'active' COMMENT 'active | closed | archived',
message_count INT DEFAULT 0 COMMENT '消息总条数',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
closed_at DATETIME COMMENT '会话结束时间',
FOREIGN KEY (tenant_id) REFERENCES tenants(id),
FOREIGN KEY (user_id) REFERENCES users(id),
INDEX idx_tenant_user (tenant_id, user_id),
INDEX idx_created (created_at)
);
-- 消息表(每条用户消息 + AI回复 = 两条记录)
CREATE TABLE chat_messages (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
message_id VARCHAR(64) NOT NULL UNIQUE COMMENT '消息唯一ID',
session_id VARCHAR(64) NOT NULL,
tenant_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
role VARCHAR(16) NOT NULL COMMENT 'user | assistant | system | tool',
content TEXT COMMENT '消息正文(文本或JSON序列化)',
content_type VARCHAR(32) DEFAULT 'text' COMMENT 'text | image_url | audio | file | tool_call',
model VARCHAR(64) COMMENT '调用的模型',
provider VARCHAR(32) COMMENT '平台或第三方提供商',
is_custom_key TINYINT(1) DEFAULT 0 COMMENT '是否使用用户自有Key',
custom_key_id BIGINT COMMENT '自有Key记录ID',
input_tokens INT DEFAULT 0,
output_tokens INT DEFAULT 0,
latency_ms INT COMMENT '响应耗时(毫秒)',
status VARCHAR(16) DEFAULT 'success' COMMENT 'success | error | timeout | rate_limited',
error_code VARCHAR(64),
error_msg TEXT,
client_version VARCHAR(32),
ip_address VARCHAR(45),
device_info VARCHAR(255),
reply_to_id VARCHAR(64) COMMENT '回复哪条消息的ID',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (tenant_id) REFERENCES tenants(id),
FOREIGN KEY (user_id) REFERENCES users(id),
INDEX idx_session_time (session_id, created_at),
INDEX idx_tenant_time (tenant_id, created_at)
);
-- 工具调用日志(Agent 调用工具时的详细记录)
CREATE TABLE tool_call_logs (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
message_id VARCHAR(64) NOT NULL COMMENT '关联消息ID',
session_id VARCHAR(64) NOT NULL,
tenant_id BIGINT NOT NULL,
tool_name VARCHAR(128) NOT NULL,
tool_call_id VARCHAR(64),
tool_input TEXT COMMENT '工具输入参数(JSON)',
tool_output TEXT COMMENT '工具输出结果(JSON)',
status VARCHAR(16) DEFAULT 'success' COMMENT 'success | error',
error_msg TEXT,
latency_ms INT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_session (session_id),
INDEX idx_tool_name (tool_name),
INDEX idx_created (created_at)
);
-- 用户反馈表(thumbs up/down 或评分)
CREATE TABLE message_feedback (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
message_id VARCHAR(64) NOT NULL,
user_id BIGINT NOT NULL,
rating VARCHAR(8) COMMENT 'thumbs_up | thumbs_down | happy | sad | angry',
comment TEXT COMMENT '用户可选的反馈文字',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id),
UNIQUE KEY uk_message_user (message_id, user_id)
);
12.3 日志记录时机
┌─────────────────────────────────────────────────────────────────┐
│ 消息记录时机 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ① 用户发消息 → 写入 chat_messages(role=user) │
│ │
│ ② AI 开始处理 → 记录处理开始时间、使用的模型/Key │
│ │
│ ③ AI 流式返回 → 每收到一个 delta chunk → 更新 output_tokens │
│ │
│ ④ AI 返回完成 → 写入 chat_messages(role=assistant) │
│ 包含:content / tokens / latency_ms / status │
│ │
│ ⑤ Agent 调用工具 → 写入 tool_call_logs │
│ │
│ ⑥ 用户反馈 → 写入 message_feedback │
│ │
│ ⑦ 会话结束(超时/主动关闭)→ 更新 chat_sessions │
│ status='closed', closed_at=NOW() │
│ │
└─────────────────────────────────────────────────────────────────┘
12.4 日志记录伪代码
async def log_message(
session_id: str,
tenant_id: int,
user_id: int,
role: str, # user | assistant | tool
content: Any,
content_type: str = "text",
metadata: Optional[dict] = None,
):
"""统一消息记录入口"""
message_id = f"msg_{secrets.token_hex(16)}"
await db.execute("""
INSERT INTO chat_messages
(message_id, session_id, tenant_id, user_id, role,
content, content_type, model, provider, is_custom_key,
custom_key_id, input_tokens, output_tokens, latency_ms,
status, error_code, error_msg, client_version, ip_address,
device_info, reply_to_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
message_id, session_id, tenant_id, user_id, role,
serialize_content(content), content_type,
metadata.get("model"),
metadata.get("provider"),
metadata.get("is_custom_key", 0),
metadata.get("custom_key_id"),
metadata.get("input_tokens", 0),
metadata.get("output_tokens", 0),
metadata.get("latency_ms", 0),
metadata.get("status", "success"),
metadata.get("error_code"),
metadata.get("error_msg"),
metadata.get("client_version"),
metadata.get("ip_address"),
metadata.get("device_info"),
metadata.get("reply_to_id"),
)
# 更新会话计数
await db.execute("""
UPDATE chat_sessions
SET message_count = message_count + 1,
updated_at = NOW()
WHERE session_id = ?
""", session_id)
return message_id
async def log_tool_call(
message_id: str,
session_id: str,
tenant_id: int,
tool_name: str,
tool_call_id: str,
tool_input: dict,
tool_output: Any,
latency_ms: int,
status: str = "success",
error_msg: str = None,
):
await db.execute("""
INSERT INTO tool_call_logs
(message_id, session_id, tenant_id, tool_name, tool_call_id,
tool_input, tool_output, status, error_msg, latency_ms)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
message_id, session_id, tenant_id, tool_name, tool_call_id,
json.dumps(tool_input),
json.dumps(tool_output) if isinstance(tool_output, dict) else str(tool_output),
status, error_msg, latency_ms
)
12.5 日志查询接口
# 获取用户会话列表
GET /api/sessions?tenant_id=x&user_id=y&limit=20&offset=0
# 获取会话详情(含消息历史)
GET /api/sessions/{session_id}/messages?limit=50&offset=0
# 搜索会话内容
GET /api/sessions/search?q=关键词&tenant_id=x&user_id=y
# 获取统计数据
GET /api/analytics/overview?tenant_id=x&start=2026-05-01&end=2026-05-04
# 返回:总消息数 / 总会话数 / 平均响应耗时 / top工具调用 / top问题
12.6 数据保留策略
| 数据类型 | 保留时间 | 说明 |
|---|---|---|
| chat_sessions | 永久 | 用户主动删除才删除 |
| chat_messages | 90天 | 敏感数据自动脱敏后保留 |
| tool_call_logs | 30天 | 日志量大,定期清理 |
| message_feedback | 永久 | 用于模型优化 |
| 异常日志 | 180天 | 用于风控分析 |
超出保留期后,优先归档到冷存储而非直接删除,支持按需恢复。
十三、微信命令屏蔽方案
13.1 需求背景
Hermes 微信平台默认开放了所有 / 开头的命令(如 /reset、/new、/model、/tools 等)。在 App 场景下,部分命令:
- 可能干扰用户体验(如
/steer改变 Agent 行为) - 存在安全风险(如
/exec执行系统命令) - 在 App 内无意义(如
/help查看帮助)
因此需要在 App 端屏蔽大部分命令,仅保留两个高频核心命令。
13.2 命令白名单
# App 端允许的命令(白名单)
ALLOWED_COMMANDS = {
"/reset": "重置当前会话上下文,保留会话历史仅清除 Agent 记忆",
"/new": "开启一个新的会话,保留当前会话记录",
}
# App 端屏蔽的命令(黑名单)
BLOCKED_COMMANDS = {
# 会话类(危险)
"/reset_all": "危险:清除所有会话",
"/clear": "危险:清空上下文",
# 系统类(危险)
"/exec": "危险:执行系统命令",
"/bash": "危险:执行 shell 命令",
"/sudo": "危险:提权操作",
# 模型类(干扰体验)
"/model": "在 App 内无需切换模型",
"/provider": "在 App 内无需切换 Provider",
# 工具类(干扰体验)
"/tools": "App 内无需查看工具列表",
"/tool": "App 内无需手动调用工具",
# 配置类(干扰体验)
"/set": "App 内不支持自定义配置",
"/config": "App 内不支持修改配置",
# Agent 引导类(干扰体验)
"/steer": "App 内不支持引导 Agent 行为",
"/persona": "App 内无需切换人格",
# 调试类(生产环境禁用)
"/debug": "生产环境禁用调试",
"/verbose": "生产环境禁用详细日志",
"/trace": "生产环境禁用跟踪",
# 信息类(无意义)
"/help": "App 内无需命令行帮助",
"/usage": "App 内无需查看用量",
"/stats": "App 内无需查看统计",
}
13.3 拦截实现方案
方案 A:在 App 端拦截(推荐)
// App 端消息发送前拦截
function preprocessMessage(text) {
const allowed = ["/reset", "/new"];
if (!text.startsWith("/")) {
return text; // 普通消息不过滤
}
const cmd = text.trim().split(/\s+/)[0].toLowerCase();
if (allowed.includes(cmd)) {
return text; // 白名单命令放行
}
// 屏蔽命令:回显提示,不发送给 Hermes
return null; // 或返回 "此命令在 App 内不可用"
}
// 发送逻辑
async function sendMessage(text) {
const processed = preprocessMessage(text);
if (processed === null) {
showToast("此命令在 App 内不可用");
return;
}
await sendToHermes(processed);
}
方案 B:在 api_server 中间件拦截
# 在 api_server 的 chat completions 处理前拦截
ALLOWED_COMMANDS = {"/reset", "/new"}
BLOCKED_COMMANDS = {
"/exec", "/bash", "/sudo", "/reset_all", "/clear",
"/model", "/provider", "/tools", "/tool",
"/set", "/config", "/steer", "/persona",
"/debug", "/verbose", "/trace",
"/help", "/usage", "/stats",
}
@app.middleware
async def block_commands(request, call_next):
if request.path != "/v1/chat/completions":
return await call_next(request)
body = await request.json()
last_message = body.get("messages", [])[-1]
content = last_message.get("content", "")
if isinstance(content, str) and content.startswith("/"):
cmd = content.strip().split()[0].lower()
if cmd in BLOCKED_COMMANDS:
return web.json_response({
"error": {
"code": "command_blocked",
"message": f"命令 /{cmd.lstrip('/')} 在 App 内已被屏蔽,仅支持 /reset 和 /new"
}
}, status=400)
return await call_next(request)
方案 C:在 Hermes 配置层屏蔽(平台级)
# config.yaml 中平台配置
platforms:
weixin:
enabled: true
command_whitelist:
- /reset
- /new
command_blacklist:
- /exec
- /bash
- /sudo
- /steer
- /model
- /tools
- /debug
- /verbose
# 命中黑名单时的响应
blocked_response: "此命令在 App 内不可用"
13.4 /reset 和 /new 的具体行为定义
| 命令 | App 内的预期行为 |
|---|---|
/reset |
清除当前会话的 Agent 短期记忆(工作记忆),保留 session 和聊天记录。相当于"重新开始当前对话的思考"。 |
/new |
创建一个新的 session_id,旧会话标记为 closed。界面切换到新会话。 |
async def handle_reset(session_id: str, tenant_id: int, user_id: int) -> dict:
"""
/reset 实现:
1. 清除 AIAgent 的当前 context window
2. 在 chat_sessions 中记录 reset 事件
3. 发送系统消息:"已重置,可以重新开始"
"""
# 通知 Hermes 清除会话记忆
await hermes.clear_session_context(session_id)
# 写入系统消息
await log_message(
session_id=session_id,
tenant_id=tenant_id,
user_id=user_id,
role="system",
content="会话已重置,之前的上下文已清除。有什么可以帮你的?",
content_type="text",
)
return {"status": "reset", "session_id": session_id}
async def handle_new(tenant_id: int, user_id: int) -> dict:
"""
/new 实现:
1. 关闭旧会话(status=closed)
2. 创建新 session_id
3. 初始化新会话记录
4. 返回新 session_id 给 App 切换
"""
old_session_id = get_current_session_id(user_id)
# 关闭旧会话
if old_session_id:
await db.execute("""
UPDATE chat_sessions
SET status='closed', closed_at=NOW()
WHERE session_id=?
""", old_session_id)
# 创建新会话
new_session_id = f"sess_{secrets.token_hex(16)}"
await db.execute("""
INSERT INTO chat_sessions (session_id, tenant_id, user_id, title, status)
VALUES (?, ?, ?, '新会话', 'active')
""", new_session_id, tenant_id, user_id)
return {"status": "new_session", "session_id": new_session_id}
13.5 统一响应格式
无论屏蔽还是执行,结果统一返回给 App:
{
"type": "command_response",
"command": "/reset",
"status": "success", // success | blocked | error
"message": "会话已重置",
"session_id": "sess_xxx", // /new 时返回新 session_id
"data": {}
}